#ifndef CLASS_NCTL_IALLOCATOR
#define CLASS_NCTL_IALLOCATOR

#include <cstdint>
#include <new>
#include <ncine/common_defines.h>
#include <ncine/allocators_config.h>
#include "utility.h"

#ifdef RECORD_ALLOCATIONS
	#include <ncine/TimeStamp.h>
#endif

namespace nctl {

/// The allocator interface
class DLL_PUBLIC IAllocator
{
  public:
	/// 16 bytes default alignment on 64bit and 8 bytes on 32bit
	static const uint8_t DefaultAlignment = 2 * sizeof(void *);

	/// Maximum length for the allocator name string
	static const unsigned int MaxNameLength = 64;

#ifdef RECORD_ALLOCATIONS
	struct Entry
	{
		ncine::TimeStamp timestamp;
		void *ptr = nullptr;
		size_t bytes = 0;
		/// Deallocations are entries with an alignment value of zero
		uint8_t alignment = DefaultAlignment;
		size_t usedMemory;
		size_t numAllocations;
	};

	inline bool recordAllocations() const { return recordAllocations_; }
	inline void setRecordAllocations(bool recordAllocations) { recordAllocations_ = recordAllocations; }

	inline const Entry &entry(size_t index) const { return entries_[index]; }
	inline size_t numEntries() const { return numEntries_; }

	static const size_t InvalidEntryIndex = static_cast<size_t>(~0);
	size_t findAllocation(void *ptr);
	size_t findAllocationBeforeIndex(void *ptr, size_t index);
	size_t findDeallocation(size_t index);
	void printEntriesCSV();
	void countNotFreed();
	void printPointerCounters();
#endif

	using AllocateFunction = void *(*)(IAllocator *allocator, size_t, uint8_t);
	using ReallocateFunction = void *(*)(IAllocator *allocator, void *, size_t, uint8_t, size_t &);
	using DeallocateFunction = void (*)(IAllocator *allocator, void *);

	IAllocator(const char *name, AllocateFunction allocFunc, ReallocateFunction reallocFunc, DeallocateFunction deallocFunc)
	    : IAllocator(name, allocFunc, reallocFunc, deallocFunc, 0, nullptr) {}
	IAllocator(const char *name, AllocateFunction allocFunc, ReallocateFunction reallocFunc, DeallocateFunction deallocFunc, size_t size, void *base);

	/// Tries to allocate the specified amount of memory with the specified alignment requirement
	inline void *allocate(size_t bytes, uint8_t alignment) { return (*allocateFunc_)(this, bytes, alignment); }
	inline void *allocate(size_t bytes) { return (*allocateFunc_)(this, bytes, DefaultAlignment); }
	/// Tries to reallocate the allocation at the specified pointer with a different size
	void *reallocate(void *ptr, size_t bytes, uint8_t alignment);
	inline void *reallocate(void *ptr, size_t bytes) { return reallocate(ptr, bytes, DefaultAlignment); }
	/// Deallocates the allocation at the specified pointer
	inline void deallocate(void *ptr) { (*deallocateFunc_)(this, ptr); }

	/// Returns the name of the allocator
	inline const char *name() const { return name_; }
	/// Returns the size of the buffer used for allocations
	inline size_t size() const { return size_; }
	/// Returns the address of the buffer used for allocations
	inline void *base() const { return base_; }
	/// Returns the amount of memory in use
	//! \note It can be more than the memory used by allocations due to overhead */
	inline size_t usedMemory() const { return usedMemory_; }
	/// Returns the amount of memory available in the buffer
	//! It can be less than the memory availble to allocations due to overhead or fragmentation */
	inline size_t freeMemory() const { return size_ - usedMemory_; }
	/// Returns the number of active allocations
	inline size_t numAllocations() const { return numAllocations_; }

	/// Returns the state of the copy on reallocation flag
	inline bool copyOnReallocation() const { return copyOnReallocation_; }
	/// Sets the state of the copy on reallocation flag
	/*! \note When the flag is true a growing reallocation might move the data in a new place */
	inline void setCopyOnReallocation(bool value) { copyOnReallocation_ = value; }

	template <class T, typename... Args> T *newObject(Args &&... args);
	template <class T> void deleteObject(T *ptr);
	template <class T> T *newArray(size_t numElements);
	template <class T> void deleteArray(T *ptr);

  protected:
	AllocateFunction allocateFunc_;
	ReallocateFunction reallocateFunc_;
	DeallocateFunction deallocateFunc_;

	char name_[MaxNameLength];
	size_t size_;
	void *base_;
	size_t usedMemory_;
	size_t numAllocations_;
	bool copyOnReallocation_;

#if defined(RECORD_ALLOCATIONS) || defined(WITH_TRACY)
	AllocateFunction realAllocateFunc_;
	ReallocateFunction realReallocateFunc_;
	DeallocateFunction realDeallocateFunc_;

	static void *wrapAllocate(IAllocator *allocator, size_t bytes, uint8_t alignment);
	static void *wrapReallocate(IAllocator *allocator, void *ptr, size_t bytes, uint8_t alignment, size_t &oldSize);
	static void wrapDeallocate(IAllocator *allocator, void *ptr);
#endif

#ifdef RECORD_ALLOCATIONS
	bool recordAllocations_;
	static const size_t MaxEntries = 128 * 1024;
	Entry entries_[MaxEntries];
	size_t numEntries_;
#endif

	friend class ProxyAllocator;
};

namespace detail {

	/// A container for functions to allocate and construct objects and arrays of objects
	template <bool value>
	struct allocHelpers
	{
		template <class T, typename... Args>
		inline static T *newObject(IAllocator &alloc, Args &&... args)
		{
			return new (alloc.allocate(sizeof(T))) T(nctl::forward<Args>(args)...);
		}

		template <class T>
		inline static void deleteObject(IAllocator &alloc, T *ptr)
		{
			ptr->~T();
			alloc.deallocate(ptr);
		}

		template <class T>
		inline static T *newArray(IAllocator &alloc, size_t numElements)
		{
			uint8_t headerSize = sizeof(size_t) / sizeof(T);

			if (sizeof(size_t) % sizeof(T) > 0)
				headerSize += 1;

			// Allocate extra space to store array length in the bytes before the array
			T *ptr = static_cast<T *>(alloc.allocate(sizeof(T) * (numElements + headerSize))) + headerSize;
			*reinterpret_cast<size_t *>(uintptr_t(ptr) - sizeof(size_t)) = numElements;

			for (size_t i = 0; i < numElements; i++)
				new (ptr + i) T();

			return ptr;
		}

		template <class T>
		inline static void deleteArray(IAllocator &alloc, T *ptr)
		{
			const size_t numElements = *reinterpret_cast<size_t *>(uintptr_t(ptr) - sizeof(size_t));

			for (size_t i = numElements; i > 0; i--)
				(ptr + i - 1)->~T();

			// Calculate how much extra memory was allocated to store the length before the array
			uint8_t headerSize = sizeof(size_t) / sizeof(T);
			if (sizeof(size_t) % sizeof(T) > 0)
				headerSize += 1;
			alloc.deallocate(ptr - headerSize);
		}
	};

	/// Specialization for trivially constructible and destructible types
	template <>
	struct allocHelpers<true>
	{
		template <class T, typename... Args>
		inline static T *newObject(IAllocator &alloc, Args &&... args)
		{
			// Trivially constructible types should still got their arguments forwarded
			return new (alloc.allocate(sizeof(T))) T(nctl::forward<Args>(args)...);
		}

		template <class T>
		inline static void deleteObject(IAllocator &alloc, T *ptr)
		{
			alloc.deallocate(ptr);
		}

		template <class T>
		inline static T *newArray(IAllocator &alloc, size_t numElements)
		{
			uint8_t headerSize = sizeof(size_t) / sizeof(T);

			if (sizeof(size_t) % sizeof(T) > 0)
				headerSize += 1;

			// Allocate extra space to store array length in the bytes before the array
			T *ptr = static_cast<T *>(alloc.allocate(sizeof(T) * (numElements + headerSize))) + headerSize;
			*reinterpret_cast<size_t *>(uintptr_t(ptr) - sizeof(size_t)) = numElements;

			return ptr;
		}

		template <class T>
		inline static void deleteArray(IAllocator &alloc, T *ptr)
		{
			// Calculate how much extra memory was allocated to store the length before the array
			uint8_t headerSize = sizeof(size_t) / sizeof(T);
			if (sizeof(size_t) % sizeof(T) > 0)
				headerSize += 1;
			alloc.deallocate(ptr - headerSize);
		}
	};

}

template <class T, typename... Args>
T *IAllocator::newObject(Args &&... args)
{
	return detail::allocHelpers<isTriviallyConstructible<T>::value>::template newObject<T>(*this, nctl::forward<Args>(args)...);
}

template <class T>
void IAllocator::deleteObject(T *ptr)
{
	if (ptr)
		detail::allocHelpers<isTriviallyDestructible<T>::value>::deleteObject(*this, ptr);
}

template <class T>
T *IAllocator::newArray(size_t numElements)
{
	if (numElements == 0)
		return nullptr;

	return detail::allocHelpers<isTriviallyConstructible<T>::value>::template newArray<T>(*this, numElements);
}

template <class T>
void IAllocator::deleteArray(T *ptr)
{
	if (ptr)
		detail::allocHelpers<isTriviallyDestructible<T>::value>::deleteArray(*this, ptr);
}

}

#endif
